package com.chencl.cipher.annotation.aspect;

import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.net.UnknownHostException;
import java.util.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.alibaba.fastjson.JSON;
import com.chencl.cipher.config.log.LogAspect;
import com.chencl.cipher.config.log.OperationLog;
import com.chencl.cipher.config.log.OperationLogDetail;
import com.chencl.cipher.dao.OperationLogMapper;
import com.chencl.cipher.dto.RequestBodyDTO;
import com.chencl.cipher.service.OperationLogService;
import com.chencl.cipher.service.SecretKeyService;
import org.apache.commons.lang3.StringUtils;
import org.apache.tomcat.util.codec.binary.Base64;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.aop.ProxyMethodInvocation;
import org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import com.alibaba.fastjson.JSONObject;
import com.chencl.cipher.annotation.Encrypt;
import com.chencl.cipher.cache.SecretUserCache;
import com.chencl.cipher.config.SecretKeyConfig;
import com.chencl.cipher.dto.ResponseBodyDTO;
import com.chencl.cipher.dto.SecretDataDTO;
import com.chencl.cipher.entity.SecretKeyEntity;
import com.chencl.cipher.exception.CryptErrorException;
import com.chencl.cipher.util.AESUtils;
import com.chencl.cipher.util.MD5Util;
import com.chencl.cipher.util.RSAUtils;
import com.chencl.cipher.util.RandomUtils;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

/**
* 解密及验证过程如下
 * 1、用接受者的私钥解密随机秘钥
 * 2、用解密出来的随机秘钥解密加密原文
 * 3、用发送方的公钥解密数字签名，获取原文摘要
 * 4、用第2步解密出来的原文生成摘要与第3步获取到的原文摘要进行比较判断内容是否被篡改，内容一致则验签通过
 *
 * 加密过程
 * 1、生成随机秘钥
 * 2、对发送数据生成消息摘要
 * 3、AES对称加密发送数据
 * 4、消息摘要用己方（发送方）私钥加密作为数字签名
 * 5、随机秘钥用接收方公钥加密
 *
 * @ClassName:  De2EncryptAspect
 * @Description:TODO(描述这个类的作用)
 * @author: Ccl
 * @date:   2021年11月27日 下午3:34:11
 * @Copyright:
 */
@Aspect
@Slf4j
@Component
@Order(1)
public class De2EncryptAspect {

	@Autowired
	SecretKeyConfig secretKeyConfig;
	@Autowired
	private SecretKeyService secretKeyService;
	@Autowired
	private OperationLogService operationLogService;

	@Around(value = "@annotation(com.chencl.cipher.annotation.Decrypt)")
	public Object decryptAround(ProceedingJoinPoint joinPoint)  throws Throwable{
		Object res = joinPoint.proceed();
		RequestAttributes ra = RequestContextHolder.getRequestAttributes();
		ServletRequestAttributes sra = (ServletRequestAttributes) ra;
		HttpServletRequest request = sra.getRequest();
		String ip = De2EncryptAspect.getClientIp(request);
		if(secretKeyConfig == null || StringUtils.isBlank(secretKeyConfig.getAppid())){
			errorException(joinPoint,res,"解密切面[De2EncryptAspect]--》请确定启动配置文件中是否配置了本机appid!");
		}

		long startTime = System.currentTimeMillis();
		Object target = joinPoint.getTarget();// 返回被织入增强处理的目标对象
		Object[] args = joinPoint.getArgs(); // 获取目标对象方法参数
		Map<String, Object> requestMap = parameterMap(joinPoint);//请求参数
		MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
	    String methodName = methodSignature.getName();//方法名

	    MethodInvocationProceedingJoinPoint methodPjp = (MethodInvocationProceedingJoinPoint)joinPoint;
	    boolean isEncrypt = false;//返回数据是否加密
	    try {
			Field field = methodPjp.getClass().getDeclaredField("methodInvocation");
			field.setAccessible(true);
			ProxyMethodInvocation invocation = (ProxyMethodInvocation) field.get(methodPjp);
			isEncrypt = invocation.getMethod().isAnnotationPresent(Encrypt.class);
		} catch (Exception e2) {
			// TODO Auto-generated catch block
			e2.printStackTrace();
		}

		log.info("进入解密接口服务，目标类：{}，方法：{},请求参数:{}", target.getClass(), methodName,JSONObject.toJSONString(requestMap));

		Object returnValue = null;//返回结果
		RequestBodyDTO data = null;//接受请求数据
		for(String key : requestMap.keySet()){
			data = JSONObject.parseObject(JSONObject.toJSONString(requestMap.get(key)), RequestBodyDTO.class);
			if(data != null && StringUtils.isNotBlank(data.getAppid())){
				break;
			}
			data = null;
		}

		if(data == null){
			log.error("接收请求参数失败!");
			errorException(joinPoint,res,"解密切面--》接收请求参数失败!");
			//throw new CryptErrorException("解密切面--》接收请求参数失败!");
		}else{
			if(StringUtils.isEmpty(data.getSign())){
				errorException(joinPoint,res,"解密切面--》接收请求参数失败! sign 不能为空");
				//throw new CryptErrorException("解密切面--》接收请求参数失败! sign 不能为空");
			}
			if(StringUtils.isEmpty(data.getRandom_key())){
				errorException(joinPoint,res,"\"解密切面--》接收请求参数失败! random_key 不能为空\"");
				//throw new CryptErrorException("解密切面--》接收请求参数失败! random_key 不能为空");
			}
			String appid = data.getAppid();//用户标识
			String content = "";
			String sign = "";
			String random_key = "";
			try {
				if(StringUtils.isNotEmpty(data.getContent())){
					content = URLDecoder.decode(data.getContent(),"utf-8");
				}
				sign = URLDecoder.decode(data.getSign(),"utf-8");
				random_key = URLDecoder.decode(data.getRandom_key(),"utf-8");
			} catch (UnsupportedEncodingException e2) {
				// TODO Auto-generated catch block
				e2.printStackTrace();
			}

			log.info("content=[{}]",content);
			log.info("sign=[{}]",sign);
			log.info("random_key=[{}]",random_key);

			SecretKeyEntity sender = secretKeyService.getSecretKeyByAppid(appid);
			SecretKeyEntity home = secretKeyService.getSecretKeyByAppid(secretKeyConfig.getAppid());

			if(sender == null || home == null){
				errorException(joinPoint,res,"解密切面[De2EncryptAspect]--》请检查发送方或本机sender["+appid+"],home["+secretKeyConfig.getAppid()+"]的配置数据是否已加载在内存中!");
				//throw new CryptErrorException("解密切面[De2EncryptAspect]--》请检查发送方或本机sender["+appid+"],home["+secretKeyConfig.getAppid()+"]的配置数据是否已加载在内存中!");
			}

			String decode_content = "";
			String decode_sign = "";
			try {
				//用己方（接收方）私钥解密获取随机秘钥
				String decode_random_key = RSAUtils.decryptByPrivateKey(Base64.decodeBase64(random_key.getBytes("UTF-8")),home.getPrivateKey());
				//用随机秘钥解密加密原文，获取源数据
				decode_content = AESUtils.decrypt(content, decode_random_key);
				//用发送方的公钥解密获取发送方消息摘要
				decode_sign = RSAUtils.decryptByPublicKey(Base64.decodeBase64(sign.getBytes("UTF-8")),sender.getPublicKey());
				//打印解密日志
				if(secretKeyConfig.isShowLog()){
					log.info("随机秘钥加密原文:[{}],解密后原文:[{}]",random_key,decode_random_key);
					log.info("源数据加密原文:[{}],解密后原文:[{}]",content,decode_content);
					log.info("数字签名加密原文:[{}],解密后原文:[{}]",sign,decode_sign);
				}
			} catch (Exception e1) {
				// TODO Auto-generated catch block
				errorException(joinPoint,res,"解密切面--》解密失败");
				//throw new CryptErrorException("解密切面--》解密失败");
			}
			//验证消息摘要，判断是否被篡改，一致则验证通过
			if(StringUtils.isEmpty(decode_content)){
				errorException(joinPoint,res,"解密切面--》解密失败");
				//throw new CryptErrorException("解密切面--》验签失败!");
			}
			boolean isVerifi = MD5Util.MD5Encode(decode_content).equals(decode_sign);
			if (!isVerifi) {
				log.error("原文摘要:[{}],发送方摘要:[{}]",MD5Util.MD5Encode(decode_content),decode_sign);
				errorException(joinPoint,res,"解密切面--》验签失败!");
				//throw new CryptErrorException("解密切面--》验签失败!");
			}

			// 将解密后的源数据赋值给接口参数
			if (StringUtils.isNotBlank(decode_content)) {
				for (int i = 0; i < args.length; i++) {
					if (i == 0) {
						if (args[i] instanceof String) {
							args[i] = decode_content;
						} else {
							args[i] = JSONObject.parseObject(decode_content, args[i].getClass());
						}
					} else if (i == 0) {
						JSONObject reqSrcJsonOby = new JSONObject();
						reqSrcJsonOby.put("appid", appid);
						args[i] = JSONObject.parseObject(reqSrcJsonOby.toJSONString(), args[i].getClass());
					}
				}

			}
			//方法继续执行
			try {
				returnValue = joinPoint.proceed(args);
			} catch (Throwable e) {
				log.error("方法执行异常,执行结果:[{}]",e.getMessage());
				throw new CryptErrorException(e.getMessage());
			}
			long endTime = System.currentTimeMillis();
			log.info("接口执行完成，目标类：{},方法：{},返回数据:{}", target.getClass(), methodName,JSONObject.toJSONString(returnValue));
			// 对返回内容进行加密
			if (returnValue != null && isEncrypt) {
				ResponseBodyDTO response = JSONObject.parseObject(JSONObject.toJSONString(returnValue), ResponseBodyDTO.class);
				String response_data = JSONObject.toJSONString(response.getData());//返回数据
				String aesKey = RandomUtils.getUUIDRandomTrim();
				String response_content =  AESUtils.encrypt(response_data, aesKey);
				String content_digest = MD5Util.MD5Encode(response_data);
				String response_sign = RSAUtils.encryptByPrivateKey(content_digest.getBytes("UTF-8"),home.getPrivateKey());
				String response_random_key = RSAUtils.encryptByPublicKey(aesKey.getBytes("UTF-8"),sender.getPublicKey());
				String a = URLEncoder.encode(response_content,"utf-8");
				String b = URLEncoder.encode(response_sign,"utf-8");
				String c = URLEncoder.encode(response_random_key,"utf-8");
				Map<String, Object> retMap = new HashMap<>();
				retMap.put("appid", secretKeyConfig.getAppid());
				retMap.put("content", a);
				retMap.put("sign", b);
				retMap.put("random_key", c);
				response.setData(retMap);
				MethodSignature signature = (MethodSignature)joinPoint.getSignature();
				operationLogService.saveOperationLog(0,ip,res,signature.getDeclaringTypeName(),signature.getName(),joinPoint.getArgs(),"");
				return response;
			}
			log.info("接口返回成功，目标类：{},方法：{},执行时间:{}", target.getClass(), methodName,(endTime-startTime)+"ms");
			return returnValue;
		}
		return null;
	}

	public void errorException(ProceedingJoinPoint joinPoint,Object res,String msg){
		MethodSignature signature = (MethodSignature)joinPoint.getSignature();
		RequestAttributes ra = RequestContextHolder.getRequestAttributes();
		ServletRequestAttributes sra = (ServletRequestAttributes) ra;
		HttpServletRequest request = sra.getRequest();
		String ip = De2EncryptAspect.getClientIp(request);
		OperationLog operationLog = operationLogService.saveOperationLog(1,ip,res,signature.getDeclaringTypeName(),signature.getName(),joinPoint.getArgs(),msg);
		throw new CryptErrorException(msg+",errId:"+operationLog.getId());
	}

	/**
	 *
	 * @Title: getClientIp
	 * @Description: 获取请求IP地址
	 * @param: @param request
	 * @param: @return
	 * @return: String
	 * @throws
	 */
	public static String getClientIp(HttpServletRequest request) {
		String ip = request.getHeader("X-Forwarded-For");
		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getHeader("Proxy-Client-IP");
		}
		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getHeader("WL-Proxy-Client-IP");
		}
		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getRemoteAddr();
			if (ip.equals("127.0.0.1")) {
				InetAddress inet = null;
				try {
					inet = InetAddress.getLocalHost();
				} catch (UnknownHostException e) {
					e.printStackTrace();
				}
				ip = inet.getHostAddress();
			}
		}
		return ip.split(",")[0];
	}

	/**
	 *
	 * @Title: parameterMap
	 * @Description: 获取切面接口参数
	 * @param: @param pjp
	 * @param: @return
	 * @return: Map<String,Object>
	 * @throws
	 */
	private static Map<String, Object> parameterMap(ProceedingJoinPoint pjp) {
        Signature signature = pjp.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        String[] strings = methodSignature.getParameterNames();//获取该方法所有参数
        Object[] args = pjp.getArgs();
        List<String> list = Arrays.asList(strings);
        Map<String, Object> map = new HashMap<String, Object>();
        for (int i = 0; i < args.length; i++) {
        	if(!(args[i] instanceof HttpServletRequest) && !(args[i] instanceof HttpServletResponse)){ //有request参数时忽略
        		map.put(list.get(i), args[i]);
        	}
        }
        return map;
    }
}
